Fetching on Mount
In the previous lesson, we saw how to make a network request in response to an event, like a form submission. But what if we need to fetch data to populate the initial view?
For example, let's suppose we're building a weather app. We want to show the user what the current temperature is in their area:
We want to show the temperature immediately, right when the component mounts.
This is a surprisingly thorny problem. It might seem like a slight difference, fetching on mount instead of fetching on event, but it opens a whole can of worms.
First, the ergonomics are tricky. In order to fetch on mount, we'd generally use the useEffect
hook, but there are some gotchas around using async/await in an effect (covered in an upcoming lesson).
And also, if we want to solve this problem in a robust, production-ready way, there are all sorts of concerns we need to worry about, including:
- Caching the response so that it can be reused by multiple components across the app.
- Revalidating the data so that it never becomes too stale.
This is a rabbit hole we could get lost in for days. 😬
As a result, it's become standard in the community to use a tool to help with this stuff. The React docs, for example, suggest using a package like React Query or SWR. In fact, on this course platform, I use SWR to solve all of these hard problems for me!
Let's see how it works.
Intro to SWR
Here's an implementation. Feel free to poke and prod at it. We'll go over how it works below.
Code Playground
- undefinedundefined
- undefinedundefined
- {"ok": true,"temperature": -1}undefined
- {"ok": true,"temperature": -1}undefined
Let's dig into this code:
Video Summary
On line 2, we import the useSWR
function from the library:
import useSWR from 'swr';
This is a custom hook. In my opinion, this is super cool. We can essentially "borrow" their solution, and implement it in a single line of code, benefitting from the hundreds/thousands of hours the team has spent developing, battle-testing, and iterating on it.
We use it like this:
const { data, error } = useSWR(ENDPOINT, fetcher);
The useSWR
hook requires two things:
- A unique key, typically the endpoint you'd like to make a request to
- A fetcher function
People think that libraries like SWR actually make the network requests for us, but this is a misconception. No matter whether we use a library like this or not, we're still the ones writing the Fetch calls by hand. The library is agnostic when it comes to data-fetching. SWR decides when to run the request, and what to do with the result.
Here's the fetcher function:
async function fetcher(endpoint) { const response = await fetch(endpoint); const json = await response.json();
return json;}
Whatever we return from our fetcher
function will be set as the data
variable inside useSWR. In this case, json
is equal to:
{ "ok": true, "temperature": -19}
(The "temperature" is a random value.)
In our App
component, we pluck data
and error
from useSWR
, and we use these variables in the rendering. If data
exists, it means the request has resolved, and we can use it in our UI, to show the temperature.
When useSWR
calls our fetcher function, it provides the key as the 1st parameter. This might seem silly in this example, when we can use the ENDPOINT
constant directly, but the idea is that fetcher functions are meant to be generic. I can define a single fetcher
function in a “helpers.js” file and use it for every on-mount network request in the app.
Now, this probably seems like a lot of trouble. If we're still the ones making the network request, what benefit does the tool provide?
One benefit is the “revalidation”. It's right in the name; SWR stands for Stale While Revalidate.
The network request will be made immediately on mount, to fetch the original value, but it will be repeated at certain strategic times, so that the value never grows too stale.
For example, suppose we switch tabs, and we spend some time on another tab. When the user returns, useSWR
will automatically repeat the network request, to fetch a fresh value, so that the user sees the latest temperature.
This is known as revalidating. If the value has changed, it'll immediately replace the old value.
While that request is pending, however, we continue to show the old value. The stale value is shown while revalidating.
This behaviour is customizable, so we can tweak it to our exact needs. There are other benefits as well (eg. caching).
Loading and error states
Our MVP above doesn't include loading or error states. Let's see how to implement them with SWR.
Loading
In addition to providing a data
value, the useSWR
hook also tells us whether or not the request is currently loading. We can pluck out the isLoading
key:
const { data, isLoading } = useSWR(ENDPOINT, fetcher);
isLoading
is a boolean value. The initial value is true
, and it flips to false
once the fetch request has completed.
We can use that value to conditionally render a loading UI like this:
function App() { const { data, isLoading } = useSWR(ENDPOINT, fetcher);
if (isLoading) { return ( <p>Loading…</p> ); }
return ( <p> Current temperature: {typeof data.temperature === 'number' && ( <span className="temperature"> {data.temperature}°C </span> )} </p> );}
Error
To simulate an error, we can add a query parameter to the ENDPOINT
:
const ENDPOINT = 'https://jor-test-api.vercel.app/api/get-temperature?simulatedError=true';
This will cause the server to return a 500 status code 👀, instead of 200. It will also return the following JSON:
{ "error": "This request returns an error, because the “simulatedError” query parameter was specified."}
With SWR, however, neither of these things is sufficient to mark this as an error.
Remember: we manage the network request! Our fetcher
function is responsible for retrieving the data, and passing it along to SWR. If we want this to count as an error, we need to throw it:
async function fetcher(endpoint) { const response = await fetch(endpoint); const json = await response.json();
if (!json.ok) { throw json; }
return json;}
With this change done, data
will be undefined, and error
will be the object we got back from the server.
Here's the final implementation, with loading and error states:
Code Playground